SSR
Server-Side Rendering (SSR) with @nano_kit/router involves code splitting, data prefetching, and state serialization.
Code Splitting
Section titled “Code Splitting”The router supports strict code splitting using loadable (see Advanced).
In an SSR environment, handling asynchronous chunks requires specific synchronization:
- Server: Before rendering,
loadPagesmust be called to resolve allloadableroutes. This ensures the router knows exactly which page to render without waiting during the render phase. - Client: Before hydration,
loadPagemust be called for the current route. This ensures the initial page chunk is downloaded and parsed, allowing the framework to hydrate the existing HTML correctly.
Stores Preloading
Section titled “Stores Preloading”When defining a page module, you can export a storesToPreload function alongside the default component export. This function returns an array of stores (signals) that need to be serialized for SSR.
loadable automatically detects storesToPreload from the module exports.
import { signal, serializable } from '@nano_kit/store'
/* Store that needs to be serialized */export const $data = serializable('homeData', signal({ text: 'Hello World' }))
/* Export storesToPreload so the router can pick it up */export function storesToPreload() { return [$data]}
export default function Home() { return <h1>{$data().text}</h1>}Server Entry
Section titled “Server Entry”On the server, efficient rendering involves:
- Loading all route definitions (
loadPages). - Creating a virtual navigation environment (
virtualNavigation). - Resolving the current
storesToPreload. - Creating a dependency injection context (
InjectionContext). - Waiting for async tasks and serializing state (
serialize). - Rendering the app with the serialized state injected into the HTML.
import { renderToString } from 'react-dom/server'import { InjectionContext, provide, serialize } from '@nano_kit/store'import { virtualNavigation, router, loadPages} from '@nano_kit/router'import { Location$, Navigation$ } from './router'import { App, pages, StoresToPreload$ } from './App'
// Ensure all lazy pages are loaded to resolve routesawait loadPages(pages)
export async function ssr(url: string) { /* Create virtual navigation */ const [$location, navigation] = virtualNavigation(url) /* Create DI context overriding navigation tokens */ /* Location$, Navigation$ are tokens created in your app using browserNavigation$ */ const context = new InjectionContext([ provide(Location$, $location), provide(Navigation$, navigation) ]) /* Resolve storesToPreload from the context */ const storesToPreload = inject(StoresToPreload$, context) /* Wait for data and serialize state */ const serialized = await serialize(storesToPreload, context) /* Render app with serialized state injected into HTML */ const html = renderToString( <> <script dangerouslySetInnerHTML={{ __html: `window.__SERIALIZED__ = ${JSON.stringify(serialized)}` }} /> <InjectionContextProvider context={context}> <App /> </InjectionContextProvider> </> )
return html}Client Entry
Section titled “Client Entry”On the client, hydration follows a similar pattern but uses browserNavigation:
- Restore state from
window.__SERIALIZED__(Serialized$). - Identify the current route (
Location$). - Preload the specific page bundle (
loadPage). - Hydrate the root with the context.
import { hydrateRoot } from 'react-dom/client'import { InjectionContext, provide, inject, Serialized$ } from '@nano_kit/store'import { loadPage } from '@nano_kit/router'import { Location$ } from './router'import { App, pages } from './App'
async function browser() { /* Create context with serialized state */ const context = new InjectionContext([ provide(Serialized$, window.__SERIALIZED__) ]) /* Inject location to determine current route */ const $location = inject(Location$, context)
/* Load initial page bundle before hydration */ await loadPage(pages, $location().route)
hydrateRoot( document.getElementById('root'), <InjectionContextProvider context={context}> <App /> </InjectionContextProvider> )}
browser()